using Collections = System.Collections.Generic;
using Reflect = System.Reflection;
using Emit = System.Reflection.Emit;
using IO = System.IO;
using System;

public sealed class Generador
{
    Emit.ILGenerator il = null;
    Collections.Dictionary<string, Emit.LocalBuilder> symbolTable;
	Instruccion inst;

    public Generador(Instruccion inst)
	{
		this.inst = inst;
	}
	public void Generar(string moduleName)
    {
        if (IO.Path.GetFileName(moduleName) != moduleName)
        {
            throw new System.Exception("solo puede salir en el actual directorio!");
        }

        Reflect.AssemblyName name = new Reflect.AssemblyName(IO.Path.GetFileNameWithoutExtension(moduleName));
        Emit.AssemblyBuilder asmb = System.AppDomain.CurrentDomain.DefineDynamicAssembly(name, Emit.AssemblyBuilderAccess.Save);
        Emit.ModuleBuilder modb = asmb.DefineDynamicModule(moduleName);
		Emit.TypeBuilder typeBuilder = modb.DefineType(IO.Path.GetFileNameWithoutExtension(moduleName));

		Emit.MethodBuilder methb = typeBuilder.DefineMethod("Main", Reflect.MethodAttributes.Static| Reflect.MethodAttributes.Public, Reflect.CallingConventions.Standard, typeof(void), new Type[]{typeof(String[]) } );
		methb.DefineParameter(1, Reflect.ParameterAttributes.Lcid, "args");

        // CodeGenerator
        this.il = methb.GetILGenerator();
        this.symbolTable = new Collections.Dictionary<string, Emit.LocalBuilder>();

        // Go Compile!
		this.symbolTable["args"] = this.il.DeclareLocal (typeof(string[]));


		/*
		 * 	IL_0001:  ldstr " "
		 *	IL_0006:  call string[] class [mscorlib]System.Environment::GetCommandLineArgs()
		 *	IL_000b:  call string string::Join(string, string[])
		 *	IL_0010:  stloc.0 
		 *	IL_0011:  ldloc.0 
		 */
		this.il.Emit(Emit.OpCodes.Ldstr, " ");
		this.il.Emit(Emit.OpCodes.Call, typeof(System.Environment).GetMethod("GetCommandLineArgs", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, null, new System.Type[] { }, null));
		this.il.Emit(Emit.OpCodes.Call, typeof(System.String).GetMethod("Join", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, null, new System.Type[] { typeof(String), typeof(String[])}, null));
		this.Almacenar("args", typeof(string[]));

        this.GenerarInstruccion(inst);

        il.Emit(Emit.OpCodes.Ret);
        typeBuilder.CreateType();
        modb.CreateGlobalFunctions();
        asmb.SetEntryPoint(methb);
        asmb.Save(moduleName);
        this.symbolTable = null;
        this.il = null;
    }


	private void GenerarInstruccion(Instruccion stmt)
	{

		if (stmt is Secuencia) {
			Secuencia sec = (Secuencia)stmt;
			this.GenerarInstruccion (sec.Primero);
			this.GenerarInstruccion (sec.Segundo);
		} else if (stmt is Declaracion) {
			// declare a local
			Declaracion decl = (Declaracion)stmt;
			this.symbolTable [decl.Identificador] = this.il.DeclareLocal (this.TipoDeExpresion (decl.Expresion));

			// set the initial value
			Asignacion asig = new Asignacion ();
			asig.Identificador = decl.Identificador;
			asig.Expresion = decl.Expresion;
			this.GenerarInstruccion (asig);
		} else if (stmt is Asignacion) {
			Asignacion asig = (Asignacion)stmt;
			this.GenerarExpresion (asig.Expresion, this.TipoDeExpresion (asig.Expresion));
			this.Almacenar (asig.Identificador, this.TipoDeExpresion (asig.Expresion));
		} else if (stmt is Escribir) {
			// the "print" statement is an alias for System.Console.WriteLine. 
			// it uses the string case
			this.GenerarExpresion (((Escribir)stmt).Expresion, typeof(string));
			this.il.Emit (Emit.OpCodes.Call, typeof(System.Console).GetMethod ("WriteLine", new System.Type[] { typeof(string) }));
		} else if (stmt is Leer) {
			this.il.Emit (Emit.OpCodes.Call, typeof(System.Console).GetMethod ("ReadLine", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, null, new System.Type[] { }, null));

			if(this.symbolTable[((Leer)stmt).Identificador].LocalType == typeof(int)){
				this.il.Emit(Emit.OpCodes.Call, typeof(int).GetMethod("Parse", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, null, new System.Type[] { typeof(string) }, null));
				this.Almacenar(((Leer)stmt).Identificador, typeof(int));
			}else
			{
				this.Almacenar(((Leer)stmt).Identificador, typeof(string));
			}
		}
		else if (stmt is CicloPara)
		{
			// ejemplo: 
			// para x = 0 hasta 100 hacer
			//   escribir "hello";
			// fin;

			// x = 0
			CicloPara cicloFor = (CicloPara)stmt;
			Asignacion assign = new Asignacion();
			assign.Identificador = cicloFor.Identificador;
			assign.Expresion = cicloFor.Desde;
			this.GenerarInstruccion(assign);			
			// Salta hasta eval
			Emit.Label eval = this.il.DefineLabel();
			this.il.Emit(Emit.OpCodes.Br, eval);

			// Instrucciones en el cuerpo del ciclo for
			Emit.Label cuerpoFor = this.il.DefineLabel();
			this.il.MarkLabel(cuerpoFor);
			this.GenerarInstruccion(cicloFor.Cuerpo);

			// Hasta (incrementar el valor de x)
			this.il.Emit(Emit.OpCodes.Ldloc, this.symbolTable[cicloFor.Identificador]);
			this.il.Emit(Emit.OpCodes.Ldc_I4, 1);
			this.il.Emit(Emit.OpCodes.Add);
			this.Almacenar(cicloFor.Identificador, typeof(int));

			// **eval** evalua si x es igual a 100? 
			this.il.MarkLabel(eval);
			this.il.Emit(Emit.OpCodes.Ldloc, this.symbolTable[cicloFor.Identificador]);
			this.GenerarExpresion(cicloFor.Hasta, typeof(int));
			this.il.Emit(Emit.OpCodes.Blt, cuerpoFor);
		}else if( stmt is Invocacion)
		{
			Invocacion inv = (Invocacion) stmt;
			if (inv.Identificador == "escribir")
			{
				this.GenerarExpresion (inv.Expresion, typeof(string));
				this.il.Emit (Emit.OpCodes.Call, typeof(System.Console).GetMethod ("WriteLine", new System.Type[] { typeof(string) }));

			}else if (inv.Identificador == "leer")
			{
				if(!(inv.Expresion is Variable))
				{
					throw new System.Exception(String.Format("'{0}' No es un identificador valido", inv.Identificador ));
				}

				Variable vari = (Variable) inv.Expresion;

				this.il.Emit (Emit.OpCodes.Call, typeof(System.Console).GetMethod ("ReadLine", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, null, new System.Type[] { }, null));

				if(this.symbolTable[vari.Identificador].LocalType == typeof(int)){
					this.il.Emit(Emit.OpCodes.Call, typeof(int).GetMethod("Parse", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, null, new System.Type[] { typeof(string) }, null));
					this.Almacenar(vari.Identificador, typeof(int));
				}else
				{
					this.Almacenar(vari.Identificador, typeof(string));
				}


			}

			else
				throw new System.Exception(String.Format("Funcion '{0}' desconocida ", inv.Identificador ));
		}
		else
		{
			throw new System.Exception("no se sabe como generar una " + stmt.GetType().Name);
		}
	}    
    	
    private void Almacenar(string name, System.Type type)
	{
		if (this.symbolTable.ContainsKey(name))
		{
			Emit.LocalBuilder locb = this.symbolTable[name];

			if (locb.LocalType == type)
			{
				this.il.Emit(Emit.OpCodes.Stloc, this.symbolTable[name]);
			}
			else
			{
				throw new System.Exception( String.Format("{0} es de tipo {1} pero intenta alamacenar un valor de tipo {2}", name, locb.LocalType.Name, type.Name));
			}
		}
		else
		{
			throw new System.Exception(String.Format("variable '{0}' no declarada", name));
		}
	}

	private void GenerarSecuencia()
	{

	}

    private void GenerarExpresion(Expresion expr, System.Type expectedType)
	{
		System.Type deliveredType;
		
        if (expr is LiteralCadena)
		{
			deliveredType = typeof(string);
			this.il.Emit(Emit.OpCodes.Ldstr, ((LiteralCadena)expr).Valor);
		}
		else if (expr is LiteralEntero)
		{
			deliveredType = typeof(int);
			this.il.Emit(Emit.OpCodes.Ldc_I4, ((LiteralEntero)expr).Valor);
		}        
        else if (expr is Variable)
		{
			string ident = ((Variable)expr).Identificador;
			deliveredType = this.TipoDeExpresion(expr);

			if (!this.symbolTable.ContainsKey(ident))
			{
				throw new System.Exception(String.Format("variable '{0}' no declarada", ident));
			}

			this.il.Emit(Emit.OpCodes.Ldloc, this.symbolTable[ident]);
		}
		else
		{
			throw new System.Exception("no se sabe como generar una " + expr.GetType().Name);
		}

        if (deliveredType != expectedType)
        {
			if ((deliveredType == typeof(int) || deliveredType == typeof(string[]))&&
                expectedType == typeof(string))
            {
				this.il.Emit(Emit.OpCodes.Box, deliveredType);
                this.il.Emit(Emit.OpCodes.Callvirt, typeof(object).GetMethod("ToString"));
            }
            else
            {
                throw new System.Exception("can't coerce a " + deliveredType.Name + " to a " + expectedType.Name);
            }
        }
	}	

    private System.Type TipoDeExpresion(Expresion expr)
	{
		if (expr is LiteralCadena)
		{
			return typeof(string);
		}
		else if (expr is LiteralEntero)
		{
			return typeof(int);
		}
		else if (expr is Variable)
		{
            Variable var = (Variable)expr;
		    if (this.symbolTable.ContainsKey(var.Identificador))
		    {
			    Emit.LocalBuilder locb = this.symbolTable[var.Identificador];
			    return locb.LocalType;
		    }
		    else
		    {
				throw new System.Exception(String.Format("variable '{0}' no declarada", var.Identificador));
		    }
		}
		else
		{
			throw new System.Exception("no se puede calcular el tipo de " + expr.GetType().Name);
		}
	}	
}
